/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 */
package org.opends.guitools.controlpanel.ui.components;

import static com.forgerock.opendj.util.OperatingSystem.isMacOS;

import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.tree.TreePath;

import org.opends.guitools.controlpanel.ui.renderer.TreeCellRenderer;

/**
 * The tree that is used in different places in the Control Panel (schema
 * browser, index browser or the LDAP entries browser).  It renders in a
 * different manner than the default tree (selection takes the whole width
 * of the tree, in a similar manner as happens with trees in Mac OS).
 */
public class CustomTree extends JTree
{
  private static final long serialVersionUID = -8351107707374485555L;
  private Set<MouseListener> mouseListeners;
  private JPopupMenu popupMenu;
  private final int MAX_ICON_HEIGHT = 18;

  /** Internal enumeration used to translate mouse events. */
  private enum NewEventType
  {
    MOUSE_PRESSED, MOUSE_CLICKED, MOUSE_RELEASED
  }

  @Override
  public void paintComponent(Graphics g)
  {
    int[] selectedRows = getSelectionRows();
    if (selectedRows == null)
    {
      selectedRows = new int[] {};
    }
    Insets insets = getInsets();
    int w = getWidth( )  - insets.left - insets.right;
    int h = getHeight( ) - insets.top  - insets.bottom;
    int x = insets.left;
    int y = insets.top;
    int nRows = getRowCount();
    for ( int i = 0; i < nRows; i++)
    {
      int rowHeight = getRowBounds( i ).height;
      if (isRowSelected(selectedRows, i))
      {
        g.setColor(TreeCellRenderer.selectionBackground);
      }
      else
      {
        g.setColor(TreeCellRenderer.nonselectionBackground);
      }
      g.fillRect( x, y, w, rowHeight );
      y += rowHeight;
    }
    final int remainder = insets.top + h - y;
    if ( remainder > 0 )
    {
      g.setColor(TreeCellRenderer.nonselectionBackground);
      g.fillRect(x, y, w, remainder);
    }

    boolean isOpaque = isOpaque();
    setOpaque(false);
    super.paintComponent(g);
    setOpaque(isOpaque);
  }

  private boolean isRowSelected(int[] selectedRows, int i)
  {
    for (int selectedRow : selectedRows)
    {
      if (selectedRow == i)
      {
        return true;
      }
    }
    return false;
  }

  /**
   * Sets a popup menu that will be displayed when the user clicks on the tree.
   * @param popMenu the popup menu.
   */
  public void setPopupMenu(JPopupMenu popMenu)
  {
    this.popupMenu = popMenu;
  }

  /** Default constructor. */
  public CustomTree()
  {
    putClientProperty("JTree.lineStyle", "Angled");
    // This mouse listener is used so that when the user clicks on a row,
    // the items are selected (is not required to click directly on the label).
    // This code tries to have a similar behavior as in Mac OS).
    MouseListener mouseListener = new MouseAdapter()
    {
      private boolean ignoreEvents;
      @Override
      public void mousePressed(MouseEvent ev)
      {
        if (ignoreEvents)
        {
          return;
        }
        MouseEvent newEvent = getTranslatedEvent(ev);

        if (isMacOS() && ev.isPopupTrigger() &&
            ev.getButton() != MouseEvent.BUTTON1)
        {
          MouseEvent baseEvent = ev;
          if (newEvent != null)
          {
            baseEvent = newEvent;
          }
          int mods = baseEvent.getModifiersEx();
          mods &= InputEvent.ALT_DOWN_MASK | InputEvent.META_DOWN_MASK |
              InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK;
          mods |=  InputEvent.BUTTON1_DOWN_MASK;
          final MouseEvent  macEvent = new MouseEvent(
              baseEvent.getComponent(),
              baseEvent.getID(),
                System.currentTimeMillis(),
                mods,
                baseEvent.getX(),
                baseEvent.getY(),
                baseEvent.getClickCount(),
                false,
                MouseEvent.BUTTON1);
          // This is done to select the node when the user does a right
          // click on Mac OS.
          notifyNewEvent(macEvent, NewEventType.MOUSE_PRESSED);
        }

        if (ev.isPopupTrigger()
            && popupMenu != null
            && (getPathForLocation(ev.getPoint().x, ev.getPoint().y) != null
                || newEvent != null))
        {
          popupMenu.show(ev.getComponent(), ev.getX(), ev.getY());
        }
        if (newEvent != null)
        {
          notifyNewEvent(newEvent, NewEventType.MOUSE_PRESSED);
        }
      }

      @Override
      public void mouseReleased(MouseEvent ev)
      {
        if (ignoreEvents)
        {
          return;
        }
        MouseEvent newEvent = getTranslatedEvent(ev);
        if (ev.isPopupTrigger()
            && popupMenu != null
            && !popupMenu.isVisible()
            && (getPathForLocation(ev.getPoint().x, ev.getPoint().y) != null
                || newEvent != null))
        {
          popupMenu.show(ev.getComponent(), ev.getX(), ev.getY());
        }

        if (newEvent != null)
        {
          notifyNewEvent(newEvent, NewEventType.MOUSE_RELEASED);
        }
      }

      @Override
      public void mouseClicked(MouseEvent ev)
      {
        if (ignoreEvents)
        {
          return;
        }
        MouseEvent newEvent = getTranslatedEvent(ev);
        if (newEvent != null)
        {
          notifyNewEvent(newEvent, NewEventType.MOUSE_CLICKED);
        }
      }

      private void notifyNewEvent(MouseEvent newEvent, NewEventType type)
      {
        ignoreEvents = true;
        // New ArrayList to avoid concurrent modifications (the listeners
        // could be unregistering themselves).
        for (MouseListener mouseListener :
          new ArrayList<MouseListener>(mouseListeners))
        {
          if (mouseListener != this)
          {
            switch (type)
            {
            case MOUSE_RELEASED:
              mouseListener.mouseReleased(newEvent);
              break;
            case MOUSE_CLICKED:
              mouseListener.mouseClicked(newEvent);
              break;
            default:
              mouseListener.mousePressed(newEvent);
            }
          }
        }
        ignoreEvents = false;
      }

      private MouseEvent getTranslatedEvent(MouseEvent ev)
      {
        MouseEvent newEvent = null;
        int x = ev.getPoint().x;
        int y = ev.getPoint().y;
        if (getPathForLocation(x, y) == null)
        {
          TreePath path = getWidePathForLocation(x, y);
          if (path != null)
          {
            Rectangle r = getPathBounds(path);
            if (r != null)
            {
              int newX = r.x + r.width / 2;
              int newY = r.y + r.height / 2;
              // Simulate an event
              newEvent = new MouseEvent(
                  ev.getComponent(),
                  ev.getID(),
                  ev.getWhen(),
                  ev.getModifiersEx(),
                  newX,
                  newY,
                  ev.getClickCount(),
                  ev.isPopupTrigger(),
                  ev.getButton());
            }
          }
        }
        return newEvent;
      }
    };
    addMouseListener(mouseListener);
    if (getRowHeight() <= MAX_ICON_HEIGHT)
    {
      setRowHeight(MAX_ICON_HEIGHT + 1);
    }
  }

  @Override
  public void addMouseListener(MouseListener mouseListener)
  {
    super.addMouseListener(mouseListener);
    if (mouseListeners == null)
    {
      mouseListeners = new HashSet<>();
    }
    mouseListeners.add(mouseListener);
  }

  @Override
  public void removeMouseListener(MouseListener mouseListener)
  {
    super.removeMouseListener(mouseListener);
    mouseListeners.remove(mouseListener);
  }

  private TreePath getWidePathForLocation(int x, int y)
  {
    TreePath path = null;
    TreePath closestPath = getClosestPathForLocation(x, y);
    if (closestPath != null)
    {
      Rectangle pathBounds = getPathBounds(closestPath);
      if (pathBounds != null &&
         x >= pathBounds.x && x < getX() + getWidth() &&
         y >= pathBounds.y && y < pathBounds.y + pathBounds.height)
      {
        path = closestPath;
      }
    }
    return path;
  }
}
